﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics.Contracts;
using Microsoft.ApplicationServer.Caching;
using YasharEl.Infrastructure.Caching.Expirations;

namespace YasharEl.Infrastructure.Caching.AppFabric
{
    public class AppFabricCacheManager : ICacheManager
    {
        #region ReadOnly Fields

        private readonly string _regionName;
        private readonly ISerializationProvider _serializationProvider;
        private readonly DataCache _dataCache;

        #endregion

        #region Protected Properties

        protected DataCache DataCache
        {
            get { return _dataCache; }
        }

        #endregion

        #region Constructors

        public AppFabricCacheManager(string name, ISerializationProvider serializationProvider, string regionName, DataCache dataCache)
        {
            Contract.Requires<ArgumentNullException>(!string.IsNullOrEmpty(name), "name could not be null or empty.");
            Contract.Requires<ArgumentNullException>(!string.IsNullOrEmpty(regionName), "regionName could not be null or empty.");
            Contract.Requires<ArgumentNullException>(dataCache != null, "dataCache could not be null.");

            Name = name;
            _regionName = regionName;
            _serializationProvider = serializationProvider;
            _dataCache = dataCache;
        }

        #endregion

        #region Properties

        public string RegionName
        {
            get { return _regionName; }
        }

        public string Name
        {
            get;
            private set;
        }

        #endregion

        #region ICacheManager Implementations

        public virtual void Add(string key, object value)
        {
            Add(key, value, CacheItemPriority.Normal, null, new NeverExpired());
        }

        public virtual void Add(string key, object value, CacheItemPriority scavengingPriority, ICacheItemRefreshAction refreshAction, params ICacheItemExpiration[] expirations)
        {
            // TODO: Implement expiration / refresh and prioritization of cache item
            if (Contains(key))
            {
                Put(key, value, true);
            }
            else
            {
                Add(key, value, true);
            }
        }

        public virtual bool Contains(string key)
        {
            object obj = _dataCache.Get(key);
            return obj != null;
        }

        public virtual void Flush()
        {
            foreach (string regionName in _dataCache.GetSystemRegions())
            {
                try
                {
                    _dataCache.ClearRegion(regionName);
                }
                catch (DataCacheException ex)
                {
                    if (ex.ErrorCode != DataCacheErrorCode.RegionDoesNotExist)
                        throw new CacheException("Error occurred when trying to flush the cache.", ex);
                    throw;
                }
            }
        }

        public virtual object GetData(string key)
        {
            if (key == null)
                return null;

            try
            {
                object value = _dataCache.Get(key);

                if (SerializationProvider != null && value != null)
                    value = SerializationProvider.Deserialize((byte[])value);

                return value;
            }
            catch (DataCacheException ex)
            {
                if (IsSafeToIgnore(ex) || ex.ErrorCode == DataCacheErrorCode.RegionDoesNotExist)
                    return null;
                else
                {
                    throw new CacheException("Error occurred when trying to get data from cache.", ex);
                }
            }
        }

        public virtual void Remove(string key)
        {
            try
            {
                if (Contains(key))
                {
                    _dataCache.Remove(key);
                }
            }
            catch (DataCacheException ex)
            {
                if (!IsSafeToIgnore(ex) && ex.ErrorCode != DataCacheErrorCode.RegionDoesNotExist)
                    throw new CacheException(string.Format("Error occurred when trying to remove cache item with key {0}.", key), ex);
            }
        }

        #endregion

        #region ICacheManager Properties

        public virtual ISerializationProvider SerializationProvider
        {
            get { return _serializationProvider; }
        }

        public virtual int Count
        {
            get
            {
                int counter = 0;
                foreach (string regionName in _dataCache.GetSystemRegions())
                {
                    foreach (KeyValuePair<string, object> cached in _dataCache.GetObjectsInRegion(regionName))
                    {
                        counter++;
                    }
                }
                return counter;
            }
        }

        public virtual object this[string key]
        {
            get
            {
                return GetData(key);
            }
        }


        #endregion

        #region Private Methods

        /// <summary>
        /// Checks whether the execption thrown by the AppFabric client is safe to ignore or not. Sometime sommunication between
        /// the client and server can be slow for example in which case the client will throw na exception, but we would want to
        /// ignore it.
        /// </summary>
        /// <param name="ex"></param>
        /// <returns></returns>
        private bool IsSafeToIgnore(DataCacheException ex)
        {
            return ex.ErrorCode == DataCacheErrorCode.ConnectionTerminated || ex.ErrorCode == DataCacheErrorCode.RetryLater || ex.ErrorCode == DataCacheErrorCode.Timeout;
        }

        /// <summary>
        /// Caches an item. If a serialization provider has been configured that will be used to serialize the value
        /// rather than relying on AppFanbric's default implementation. If the region the item is to be cached in doesn't
        /// exists, ite will be created.
        /// </summary>
        /// <param name="key">The key for the item to be cached.</param>
        /// <param name="value">The item to be cached.</param>
        /// <param name="createMissingRegion">A flag to determine whether or not the cache region should be created if it
        /// doesn't exist.</param>
        private void Put(string key, object value, bool createMissingRegion)
        {
            Contract.Requires<ArgumentNullException>(!string.IsNullOrEmpty(key), "key could not be null or empty.");
            Contract.Requires<ArgumentNullException>(value != null, "value could not be null.");

            try
            {
                if (SerializationProvider != null)
                    value = SerializationProvider.Serialize(value);

                _dataCache.Put(key, value);
            }
            catch (DataCacheException ex)
            {
                if (ex.ErrorCode == DataCacheErrorCode.RegionDoesNotExist && createMissingRegion)
                    CreateAppFabricRegion(b => Put(key, value, false));
                else if (!IsSafeToIgnore(ex) && ex.ErrorCode != DataCacheErrorCode.ObjectLocked)
                {
                    throw new CacheException("Error occurred when trying to put item to cache.", ex);
                }
            }
        }

        private void Add(string key, object value, bool createMissingRegion)
        {
            Contract.Requires<ArgumentNullException>(!string.IsNullOrEmpty(key), "key could not be null or empty.");
            Contract.Requires<ArgumentNullException>(value != null, "value could not be null.");

            try
            {
                if (SerializationProvider != null)
                    value = SerializationProvider.Serialize(value);

                _dataCache.Put(key, value);
            }
            catch (DataCacheException ex)
            {
                if (ex.ErrorCode == DataCacheErrorCode.RegionDoesNotExist && createMissingRegion)
                    CreateAppFabricRegion(b => Add(key, value, false));
                else if (!IsSafeToIgnore(ex) && ex.ErrorCode != DataCacheErrorCode.ObjectLocked)
                {
                    throw new CacheException("Error occurred when trying to add item to cache.", ex);
                }
            }
        }

        /// <summary>
        /// Creates the App Fabric cache region used for caching items.
        /// </summary>
        /// <param name="callback">A call back to call once the region has been created.</param>
        private void CreateAppFabricRegion(Action<bool> callback)
        {
            CreateRegion(_dataCache, RegionName, callback);
        }

        /// <summary>
        /// Creates an AppFabric cache region.
        /// </summary>
        /// <param name="cache">The data cache [client] to use to create the cache region.</param>
        /// <param name="regionName">The name of the AppFabric cache region to create.</param>
        /// <param name="callback">A call back to call once the region has been created.</param>
        private void CreateRegion(DataCache cache, string regionName, Action<bool> callback)
        {
            try
            {
                callback(cache.CreateRegion(regionName));
            }
            catch (DataCacheException ex)
            {
                if (ex.ErrorCode == DataCacheErrorCode.RegionAlreadyExists)
                    callback(false);
                else
                {
                    throw new CacheException("Failed to create cache region.", ex);
                }
            }
        }

        #endregion
    }
}
